package com.nightpiter.model;

import static com.nightpiter.controller.log.Logger.log;
import static com.nightpiter.controller.log.Logger.logProfilerBegin;
import static com.nightpiter.controller.log.Logger.logProfilerEnd;

import java.util.ArrayList;
import java.util.Hashtable;

import android.graphics.Point;

import com.nightpiter.exceptions.BindingBuilderException;
import com.nightpiter.exceptions.LineBindingBuilderException;
import com.nightpiter.exceptions.ModelNotAssignedBuilderException;
import com.nightpiter.exceptions.StationBindingBuilderException;
import com.nightpiter.model.MetroModelState.Action;
import com.nightpiter.model.MetroModelState.Element;
import com.nightpiter.model.objects.Hub;
import com.nightpiter.model.objects.Line;
import com.nightpiter.model.objects.Station;
import com.nightpiter.model.objects.Time;
import com.nightpiter.model.objects.Transfer;
import com.nightpiter.model.objects.Tunnel;

/**
 * MetroModelBuider; realization of Builder pattern
 * 
 * Used to construct such complex object as MetroModel instance; performs
 * bind-by-id resolving between objects of Station, Line, Tunnel and Transfer
 * classes
 * 
 * suitable for SAX-, and for DOM-reader; for DOM use DomBuilderWrapper
 * 
 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
 */
public class MetroModelBuilder {
	/**
	 * temporary structure class to store raw data about Station element read
	 * from the XML
	 */
	private class StationStruct {
		public int id = 0;

		public Time open, close;

		public String name;

		public Point position, location;
	}

	/**
	 * temporary structure class to store raw data about Tunnel element read
	 * from the XML
	 */
	private class TunnelStruct {
		public int from, to, line;

		public int time;
	}

	/**
	 * temporary structure class to store raw data about Transfer element read
	 * from the XML
	 */
	private class TransferStruct {
		public int from, to;

		public int time;
	}

	/**
	 * temporary structure class to store raw data about Line element read from
	 * the XML
	 */
	private class LineStruct {
		public int id = 0, color;

		public String name;
	}

	/**
	 * this field stores a model we have to construct
	 * 
	 * it's null in the initial state
	 */
	private MetroModel model;

	/**
	 * current state of the model being built
	 */
	private MetroModelState state;

	/**
	 * this field contains data that we will pass to the model's constructor
	 */
	private Hashtable<Integer, Station> stationHash = new Hashtable<Integer, Station>();

	/**
	 * this field is needed when we construct Tunnels
	 */
	private Hashtable<Integer, Line> lineHash = new Hashtable<Integer, Line>();

	/**
	 * temporary structure to write station data in
	 */
	private StationStruct stationStruct = new StationStruct();

	/**
	 * temporary structure to write line data in
	 */
	private LineStruct lineStruct = new LineStruct();

	/**
	 * Collection of Tunnel temporary structures
	 */
	private ArrayList<TunnelStruct> tunnelStructCollection = new ArrayList<TunnelStruct>();

	/**
	 * Collection of Transfer temporary structures
	 */
	private ArrayList<TransferStruct> transferStructCollection = new ArrayList<TransferStruct>();

	/**
	 * the name of the city represented in the Model
	 */
	private String cityName = "";

	/**
	 * comment to the map
	 */
	private String comment = "not presented";
	
	/**
	 * authors' names
	 */
	private ArrayList<String> authors = new ArrayList<String>();

	// TODO this should be specified in xml for each line element
	private final static int DEFAULT_TRAIN_DELAY = 100;

	/**
	 * public constructor
	 * 
	 * @param model
	 *            the MetroModel to build
	 * @throws ModelNotAssignedBuilderException
	 */
	public MetroModelBuilder(MetroModel model)
			throws ModelNotAssignedBuilderException {
		if (model == null) {
			throw new ModelNotAssignedBuilderException();
		}
		this.model = model;
		this.state = model.getState();
	}

	/**
	 * performs bind-by-id resolving and passes the result to the model's build
	 * method
	 * 
	 * @throws StationBindingBuilderException
	 * @throws LineBindingBuilderException
	 */
	public void buildModel() throws BindingBuilderException {
		logProfilerBegin("building the Model");
		state.action = Action.BUILDING;
		state.element = Element.MAP;
		state.setBuilt(false);

		log("creating tunnels");
		ArrayList<Tunnel> tunnels = new ArrayList<Tunnel>(
				tunnelStructCollection.size());
		state.element = Element.TUNNELS;
		state.setTunnelsBuilt(false);
		for (TunnelStruct tunnelStruct : tunnelStructCollection) {
			Station fromStation = stationHash.get(tunnelStruct.from);
			if (fromStation == null) {
				throw new StationBindingBuilderException(tunnelStruct.from,
						"from", "tunnel");
			}
			Station toStation = stationHash.get(tunnelStruct.to);
			if (toStation == null) {
				throw new StationBindingBuilderException(tunnelStruct.to, "to",
						"tunnel");
			}
			Line line = lineHash.get(tunnelStruct.line);
			if (line == null) {
				throw new LineBindingBuilderException(tunnelStruct.line,
						"line", "tunnel");
			}
			tunnels.add(new Tunnel(fromStation, toStation, tunnelStruct.time,
					line));
		}
		state.element = Element.MAP;
		state.setTunnelsBuilt(true);

		log("creating transfers");
		ArrayList<Transfer> transfers = new ArrayList<Transfer>(
				transferStructCollection.size());
		state.element = Element.TRANSFERS;
		state.setTransfersBuilt(false);
		for (TransferStruct transferStruct : transferStructCollection) {
			Station fromStation = stationHash.get(transferStruct.from);
			if (fromStation == null) {
				throw new StationBindingBuilderException(transferStruct.from,
						"from", "transfer");
			}
			Station toStation = stationHash.get(transferStruct.to);
			if (toStation == null) {
				throw new StationBindingBuilderException(transferStruct.to,
						"to", "transfer");
			}
			transfers.add(new Transfer(fromStation, toStation,
					transferStruct.time));
		}
		state.element = Element.MAP;
		state.setTransfersBuilt(true);

		tunnelStructCollection = null;
		transferStructCollection = null;

		log("preparing stations");
		ArrayList<Station> stations = new ArrayList<Station>(stationHash
				.values());
		log("preparing lines");
		ArrayList<Line> lines = new ArrayList<Line>(lineHash.values());

		log("creating hubs");
		state.element = Element.HUBS;
		state.setHubsBuilt(false);
		ArrayList<Hub> hubs = this.buildHubs(transfers);
		state.element = Element.MAP;
		// state.setHubsBuilt(true);

		state.action = Action.LOADING;
		state.setBuilt(true);
		this.model.build(stations, transfers, tunnels, hubs, lines);
		this.model.setAuthors(authors);
		this.model.setCityName(cityName);
		this.model.setComment(comment);
		logProfilerEnd("building the Model");
	}

	/**
	 * builds hubs
	 * 
	 * @param transfers
	 * @return
	 */
	private ArrayList<Hub> buildHubs(ArrayList<Transfer> transfers) {
		ArrayList<Hub> hubs = new ArrayList<Hub>();
		for (Transfer transfer : transfers) {
			if (transfer.getHub() == null) {
				Hub hub = searchNeighborTransfers(transfer);
				hubs.add(hub);
			}
		}
		// extractHubComponents(hubs);
		return hubs;
	}

	/**
	 * searches all neighbor transfers graph and creates new hub if necessary;
	 * there are some analogyes with Dijkstra's algorithm but this one is
	 * simpler
	 * 
	 * @param transfer
	 * @return new hub created or null if hub already has existed
	 */
	private Hub searchNeighborTransfers(Transfer transfer) {
		ArrayList<Transfer> unvisitedNeighbors = new ArrayList<Transfer>();
		ArrayList<Transfer> hubTransfers = new ArrayList<Transfer>();
		unvisitedNeighbors.add(transfer);
		// TODO introduce double pass resolving
		while (!unvisitedNeighbors.isEmpty()) {
			Transfer neighborTransfer = unvisitedNeighbors.remove(0);

			Station toStation = neighborTransfer.getToStation();

			for (Transfer transferToAdd : toStation.getTransfers()) {
				if (!hubTransfers.contains(transferToAdd)) {
					unvisitedNeighbors.add(transferToAdd);
					hubTransfers.add(transferToAdd);
				}
			}
		}
		return new Hub(hubTransfers);
	}

	/**
	 * called when the city's name retrieved
	 * 
	 * @param cityName
	 */
	public void setCityName(String cityName) {
		this.cityName = cityName;
	}

	/**
	 * called when the author name retrieved
	 * 
	 * @param authorName
	 */
	public void setAuthorName(String authorName) {
		this.authors.add(authorName);
	}

	public void setNote(String note) {
		this.comment = note;
	}

	/**
	 * called when a new station element has occured and it's id's retrieved
	 * 
	 * @param id
	 */
	public void startStation(int id) {
		this.stationStruct.id = id;
	}

	/**
	 * sets the new station's name
	 * 
	 * @param name
	 */
	public void setStationName(String name) {
		this.stationStruct.name = name;
	}

	/**
	 * sets the new station's position
	 * 
	 * @param x
	 * @param y
	 */
	public void setStationPosition(int x, int y) {
		this.stationStruct.position = new Point(x, y);
	}

	/**
	 * sets the new station's geo location
	 * 
	 * @param x
	 * @param y
	 */
	public void setStationLocation(int x, int y) {
		this.stationStruct.location = new Point(x, y);
	}

	/**
	 * sets the new station's open and close schedule
	 * 
	 * @param open
	 * @param close
	 */
	public void setStationSchedule(Time open, Time close) {
		this.stationStruct.open = open;
		this.stationStruct.close = close;
	}

	/**
	 * instantiates Station with retrieved parameters
	 */
	public void endStation() {
		stationHash.put(stationStruct.id, new Station(stationStruct.name,
				stationStruct.position, stationStruct.location,
				stationStruct.open, stationStruct.close));
	}

	/**
	 * add's a new Tunnel to future model
	 * 
	 * @param id
	 * @param to
	 * @param from
	 * @param time
	 * @param line
	 */
	public void addTunnel(int to, int from, int time, int line) {
		TunnelStruct tunnelStruct = new TunnelStruct();
		tunnelStructCollection.add(tunnelStruct);
		tunnelStruct.to = to;
		tunnelStruct.from = from;
		tunnelStruct.line = line;
		tunnelStruct.time = time;
	}

	/**
	 * add's a new Transfer to future model
	 * 
	 * @param id
	 * @param to
	 * @param from
	 * @param time
	 */
	public void addTransfer(int to, int from, int time) {
		TransferStruct transferStruct = new TransferStruct();
		transferStructCollection.add(transferStruct);
		transferStruct.to = to;
		transferStruct.from = from;
		transferStruct.time = time;
	}

	/**
	 * called when a new line element has occured and it's id's retrieved
	 * 
	 * @param id
	 */
	public void startLine(int id) {
		this.lineStruct.id = id;
	}

	/**
	 * sets the new line's name
	 * 
	 * @param name
	 */
	public void setLineName(String name) {
		this.lineStruct.name = name;
	}

	/**
	 * sets the new line's color
	 * 
	 * @param color
	 */
	public void setLineColor(int color) {
		this.lineStruct.color = color;
	}

	/**
	 * instantiates Line with retrieved parameters
	 */
	public void endLine() {
		lineHash.put(lineStruct.id, new Line(lineStruct.name, lineStruct.color,
				DEFAULT_TRAIN_DELAY));
	}

	/**
	 * called when entering the root element
	 * 
	 */
	public void startMap() {
		state.action = Action.READING;
		state.element = Element.MAP;
		state.setRead(false);
	}

	/**
	 * called when exiting the root element
	 */
	public void endMap() {
		state.action = Action.LOADING;
		state.element = Element.MAP;
		state.setRead(true);
	}

	/**
	 * called when entering lines block
	 */
	public void startLines() {
		log("reading lines");
		state.element = Element.LINES;
		state.setLinesRead(false);
	}

	/**
	 * called when exiting lines block
	 */
	public void endLines() {
		state.element = Element.MAP;
		state.setLinesRead(true);
	}

	/**
	 * called when entering stations block
	 */
	public void startStations() {
		log("reading stations");
		state.element = Element.STATIONS;
		state.setStationsRead(false);
	}

	/**
	 * called when exiting stations block
	 */
	public void endStations() {
		state.element = Element.MAP;
		state.setStationsRead(true);
	}

	/**
	 * called when entering tunnels block
	 */
	public void startTunnels() {
		log("reading tunnels");
		state.element = Element.TUNNELS;
		state.setTunnelsRead(false);
	}

	/**
	 * called when exiting tunnels block
	 */
	public void endTunnels() {
		state.element = Element.MAP;
		state.setTunnelsRead(true);
	}

	/**
	 * called when entering transfers block
	 */
	public void startTransfers() {
		log("reading transfers");
		state.element = Element.TRANSFERS;
		state.setTransfersRead(false);
	}

	/**
	 * called when exiting transfers block
	 */
	public void endTransfers() {
		state.element = Element.MAP;
		state.setTransfersRead(true);
	}

}
